﻿using System;
using System.Data.SqlClient;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Security.AccessControl;
using System.Threading;
using System.Windows.Forms;
using DeVierKoeden.Dynamicweb.CreateSite.DataAnonymization;
using DeVierKoeden.Dynamicweb.CreateSite.Extensions;
using Microsoft.Web.Administration;
using NLog;

namespace DeVierKoeden.Dynamicweb.CreateSite
{
  public class SiteDeleter
  {
    private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

    private readonly ILogger _progresLogger;
    private readonly Site _site;
    private readonly DeleteSettings _deleteSettings;
    private string _tempFolder;

    /// <summary>
    /// Initializes a new instance of the SiteDeleter class.
    /// </summary>
    public SiteDeleter(ILogger progresLogger, Site site)
    {
      _progresLogger = progresLogger;
      _site = site ?? throw new ArgumentNullException(nameof(site), "site is null");
      _deleteSettings = GetDeleteSettings();
    }

    public void BackupSite()
    {
      BackupOrDeleteSite(BackupDeleteType.BackupOnly, null, null, new []{ "System\\Packages" }, false);
    }

    internal void BackupOrDeleteSite(BackupDeleteType action, ReplacementBehavior? replacementBehavior, string anonymizationSuffix, string[] foldersToIgnore, bool skipDatabase)
    {
      if ((action == BackupDeleteType.BackupAndDelete || action == BackupDeleteType.DeleteOnly) && !ConfirmDelete(action))
      {
        return;
      }

      if (!Directory.Exists(_deleteSettings.BackupFolder))
      {
        try
        {
          Logger.Trace("Created backup folder at {0}", _deleteSettings.BackupFolder);
          Directory.CreateDirectory(_deleteSettings.BackupFolder);
        }
        catch
        {
          // Ignore. Let user handle the case where the folder can't be created.
        }
        if (!Directory.Exists(_deleteSettings.BackupFolder))
        {
          MessageBox.Show(string.Format("Backup folder {0} not found and it could also not be created automatically", _deleteSettings.BackupFolder), "Missing backup folder", MessageBoxButtons.OK);
          return;
        }
      }
      if (action != BackupDeleteType.BackupOnly)
      {
        _progresLogger.LogProgress("Shutting down application pool...");
        ShutDownApplicationPool();
      }

      if (action == BackupDeleteType.BackupAndDelete || action == BackupDeleteType.BackupOnly)
      {
        _tempFolder = string.Format("{0}{1}", Path.GetTempPath(), Guid.NewGuid());
        Logger.Trace("Temp folder to create is {0}", _tempFolder);
        Directory.CreateDirectory(_tempFolder);
        Logger.Trace("Temp folder created at {0}.", _tempFolder);
        Helpers.AclFolder(_tempFolder, FileSystemRights.Modify, "Everyone");
        _progresLogger.LogProgress("Backing up SQL Server database...");
        if (!skipDatabase)
        {
          BackupDatabase();
          if (replacementBehavior != null)
          {
            CreateCopyForAnonymizationAndAnonymize(replacementBehavior.Value, anonymizationSuffix);
          }
        }
        _progresLogger.LogProgress("Backing up site folder...");
        BackupFilesOnDisk(action, foldersToIgnore);
      }

      if (action == BackupDeleteType.BackupAndDelete || action == BackupDeleteType.DeleteOnly)
      {
        _progresLogger.LogProgress("Deleting SQL Server database...");
        DeleteDatabase();
        _progresLogger.LogProgress("Deleting IIS Site...");
        DeleteIisSite();
        _progresLogger.LogProgress("Deleting site folder...");
        DeleteFilesOnDisk();
      }
      _progresLogger.LogProgress("Starting up application pool...");
      if (action != BackupDeleteType.BackupOnly)
      {
        StartApplicationPool();
        DeleteApplicationPool();
      }
      _progresLogger.LogProgress("Ready");
    }

    private void CreateCopyForAnonymizationAndAnonymize(ReplacementBehavior replacementBehavior, string suffix)
    {
      var sourceAndTargetBackupFileName = string.Format(@"{1}_{0}.bak", _deleteSettings.DatabaseName, DateTime.Now.ToString("yyyyMMdd"));
      var sourceAndTargetBackupFileLocation = string.Format(@"{1}\{0}", sourceAndTargetBackupFileName, _tempFolder);
      var tempName = Guid.NewGuid().ToString();

      CreateCopyForAnonymization(tempName, sourceAndTargetBackupFileLocation);

      var settings = new SimpleAnonymizerSettings
      {
        AnonymizationMethod = replacementBehavior,
        AnonymizationSuffix = suffix,
        DatabaseName = tempName,
        SqlServerInstance = _deleteSettings.DatabaseServer
      };
      var anonymizer = new DataAnonymizer(settings, _progresLogger);
      anonymizer.AnonymizeData();

      BackupDatabaseAgainAndDeleteTempCopy(sourceAndTargetBackupFileLocation, tempName);
    }

    private void BackupDatabaseAgainAndDeleteTempCopy(string sourceAndTargetBackupFileLocation, string tempName)
    {
      string backupDatabase = string.Format(@"-S{0} -E -Q""BACKUP DATABASE [{1}] TO  DISK = N'{2}' WITH COPY_ONLY, NOFORMAT, INIT,  NAME = N'OnBeforeDeleteFromDwTool', SKIP, NOREWIND, NOUNLOAD,  STATS = 10""", _deleteSettings.DatabaseServer, tempName, sourceAndTargetBackupFileLocation);
      Helpers.RunProcess("SQLCMD", backupDatabase);

      Helpers.RunProcess("SQLCMD", $@"-S{_deleteSettings.DatabaseServer} -E -Q""ALTER DATABASE [{tempName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE""");

      // Remove temp database again; the restore command below creates it at the proper location. The Single User command removes all locks / connections so
      // we can delete the database.
      Helpers.RunProcess("SQLCMD", $@"-S{_deleteSettings.DatabaseServer} -E -Q""DROP DATABASE [{tempName}]""");
    }
    private void CreateCopyForAnonymization(string tempName, string sourceAndTargetBackupFileLocation)
    {
      var databaseLocation = string.Empty;
      var logLocation = string.Empty;
      var databasePhysicalName = string.Empty;
      var databaseLogName = string.Empty;
      // Create temp database and then query physical and logical paths
      Helpers.RunProcess("SQLCMD", $@"-S{_deleteSettings.DatabaseServer} -E -Q""CREATE DATABASE [{tempName}]""");
      const string sql = "select physical_name AS PhysicalName from sys.database_files;";
      using (var sqlConnection = new SqlConnection($"data source={_deleteSettings.DatabaseServer}; integrated security=true;Initial catalog={tempName}"))
      {
        using (var sqlCommand = new SqlCommand(sql, sqlConnection))
        {
          sqlConnection.Open();
          try
          {
            using (var sqlDataReader = sqlCommand.ExecuteReader())
            {
              while (sqlDataReader.Read())
              {
                if (sqlDataReader["PhysicalName"].ToString().ToLower().EndsWith(".mdf"))
                {
                  databaseLocation = sqlDataReader["PhysicalName"].ToString();
                }
                if (sqlDataReader["PhysicalName"].ToString().ToLower().EndsWith(".ldf"))
                {
                  logLocation = sqlDataReader["PhysicalName"].ToString();
                }
              }
              sqlDataReader.Close();
            }
          }
          catch (Exception ex)
          {
            MessageBox.Show($"Error retrieving locations: {ex.Message}.");
            throw;
          }
        }
        sqlConnection.Close();
      }

      string sql2 = $"RESTORE FILELISTONLY FROM DISK = N'{sourceAndTargetBackupFileLocation}'";
      using (var sqlConnection = new SqlConnection($"data source={_deleteSettings.DatabaseServer}; integrated security=true;"))
      {
        using (var sqlCommand = new SqlCommand(sql2, sqlConnection))
        {
          sqlConnection.Open();
          try
          {
            using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader())
            {
              while (sqlDataReader.Read())
              {
                if (sqlDataReader["Type"].ToString() == "D")
                {
                  databasePhysicalName = sqlDataReader["LogicalName"].ToString();
                }
                if (sqlDataReader["Type"].ToString() == "L")
                {
                  databaseLogName = sqlDataReader["LogicalName"].ToString();
                }
              }
              sqlDataReader.Close();
            }
          }
          catch (Exception ex)
          {
            MessageBox.Show($"Error restoring database. The error message is: {ex.Message}. Canceled site creation.");
          }
        }
      }
      // The Single User command removes all locks / connections so we can delete the database.
      Helpers.RunProcess("SQLCMD", $@"-S{_deleteSettings.DatabaseServer} -E -Q""ALTER DATABASE [{tempName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE""");

      // Remove temp database; the restore command below creates it at the proper location.
      Helpers.RunProcess("SQLCMD", $@"-S{_deleteSettings.DatabaseServer} -E -Q""DROP DATABASE [{tempName}]""");

      // Create it at proper location
      string restoreCommand = $@"-S {_deleteSettings.DatabaseServer} -E -Q""RESTORE DATABASE [{tempName}] FROM  DISK = N'{sourceAndTargetBackupFileLocation}' WITH  FILE = 1,  MOVE N'{databasePhysicalName}' TO N'{databaseLocation}',  MOVE N'{databaseLogName}' TO N'{logLocation}',  NOUNLOAD,  REPLACE,  STATS = 10""";
      Helpers.RunProcess("osql.exe", restoreCommand);
      File.Delete(sourceAndTargetBackupFileLocation);
    }
    private bool ConfirmDelete(BackupDeleteType action)
    {
      string msg;
      switch (action)
      {
        case BackupDeleteType.BackupAndDelete:
          msg = "Are you sure you want to delete this site and its data? The files and database will be backed up to a ZIP file in your configured Backup folder.";
          break;
        case BackupDeleteType.DeleteOnly:
          msg = "Are you sure you want to permanently delete this site?";
          break;
        default:
          msg = "";
          break;
      }
      var result = MessageBox.Show(msg, "Are you sure?", MessageBoxButtons.YesNo);
      return result == DialogResult.Yes;
    }

    private void ShutDownApplicationPool()
    {
      //string webconfig = _site.Applications[0].Path
      ApplicationPool appPool;
      using (var serverManager = new ServerManager())
      {
        appPool = serverManager.ApplicationPools.First(x => x.Name == _site.Applications[0].ApplicationPoolName);
      }
      if (appPool.State == ObjectState.Started)
      {
        appPool.Recycle();
        appPool.Stop();
      }
    }

    private void DeleteApplicationPool()
    {
      using (var serverManager = new ServerManager())
      {
        var appPool = serverManager.ApplicationPools.First(x => x.Name == _site.Applications[0].ApplicationPoolName);
        if (appPool.Name == _deleteSettings.SiteName && !AppPoolHasSites(appPool.Name))
        {
          appPool.Delete();
          serverManager.CommitChanges();
        }
      }
    }

    private void StartApplicationPool()
    {
      var serverManager = new ServerManager();
      var appPool = serverManager.ApplicationPools.First(x => x.Name == _site.Applications[0].ApplicationPoolName);
      if (appPool.State == ObjectState.Stopped)
      {
        appPool.Start();
      }
    }

    private bool AppPoolHasSites(string appPoolName)
    {
      var manager = new ServerManager();
      foreach (Site site in manager.Sites)
      {
        foreach (var app in site.Applications)
        {
          if (app.ApplicationPoolName == appPoolName)
          {
            return true;
          }
        }
      }
      return false;
    }

    private void BackupDatabase()
    {
      string backupDatabase = string.Format(@"-S{0} -E -Q""BACKUP DATABASE [{1}] TO  DISK = N'{2}\{3}_{1}.bak' WITH COPY_ONLY, NOFORMAT, NOINIT,  NAME = N'OnBeforeDeleteFromDwTool', SKIP, NOREWIND, NOUNLOAD,  STATS = 10""", _deleteSettings.DatabaseServer, _deleteSettings.DatabaseName, _tempFolder, DateTime.Now.ToString("yyyyMMdd"));
      Helpers.RunProcess("SQLCMD", backupDatabase);
    }

    private void DeleteDatabase()
    {
      string dropDatabase = string.Format(@"-S{1} -E -Q""IF EXISTS (SELECT name FROM sys.databases WHERE name = N'{0}') DROP DATABASE [{0}] """, _deleteSettings.DatabaseName, _deleteSettings.DatabaseServer);
      Helpers.RunProcess("SQLCMD", dropDatabase);
    }

    private void DeleteFilesOnDisk()
    {
      Helpers.MakeWritable(_deleteSettings.FilesFolder);
      Directory.Delete(_deleteSettings.FilesFolder, true);
      if (_deleteSettings.IsCustomSolution)
      {
        Helpers.MakeWritable(_deleteSettings.RootFolder);
        Directory.Delete(_deleteSettings.RootFolder, true);
      }
      string parentOfFiles = Directory.GetParent(_deleteSettings.FilesFolder).FullName;
      if (!Directory.GetFileSystemEntries(parentOfFiles).Any())
      {
        Helpers.MakeWritable(parentOfFiles);
        Directory.Delete(parentOfFiles, true);
      }
      else
      {
        MessageBox.Show(string.Format("Folder {0} is not empty and has not been deleted", parentOfFiles));
      }
    }

    private void BackupFilesOnDisk(BackupDeleteType action, string[] foldersToIgnore)
    {
      string originalFileName = string.Format(@"{0}\{1}.zip", _deleteSettings.BackupFolder, _site.Name);
      string fileName = originalFileName;

      Logger.Trace("Considering {0} as backup file.", fileName);
      for (int i = 1; i < 20; i++)
      {
        if (File.Exists(fileName))
        {
          fileName = originalFileName.Replace(".zip", string.Format("_{0}.zip", i.ToString().PadLeft(2, '0')));
        }
        else
        {
          break;
        }
      }

      Logger.Trace("Using {0} as backup file.", fileName);

      Logger.Trace("About to copy files from {0} to {1}.", _deleteSettings.FilesFolder, _tempFolder + "\\Files");
      var excludePatternForFolders = "node_modules|.git";
      if (foldersToIgnore != null && foldersToIgnore.Any())
      {
        excludePatternForFolders += "|" + string.Join("|", foldersToIgnore);
      }
      Helpers.DeepCopy(new DirectoryInfo(_deleteSettings.FilesFolder), new DirectoryInfo(_tempFolder + "\\Files"), excludePatternForFolders, true);
      Logger.Trace("Copied files from {0} to {1}.", _deleteSettings.FilesFolder, _tempFolder + "\\Files");

      if (_deleteSettings.IsCustomSolution)
      {
        string appTargetFolder = _tempFolder + "\\Application";
        if (action != BackupDeleteType.BackupOnly)
        {
          string webconfigFile = _deleteSettings.RootFolder + "\\web.config";
          Logger.Trace("Overwriting web.config at {0} to release file lock. ", webconfigFile);
          File.WriteAllText(webconfigFile, File.ReadAllText(webconfigFile));
          Thread.Sleep(1000);
          Logger.Trace("Done overwriting web.config at {0} to release file lock. ", webconfigFile);
        }
        string parentOfApp = Directory.GetParent(_deleteSettings.RootFolder).FullName;
        if (Directory.GetFiles(parentOfApp, "*.sln").Any())
        {
          Logger.Trace("Copying app files from {0} to {1}. ", parentOfApp, _tempFolder);
          Helpers.DeepCopy(new DirectoryInfo(parentOfApp), new DirectoryInfo(_tempFolder), "node_modules|.git", true);
          Logger.Trace("Done copying app files from {0} to {1}. ", parentOfApp, _tempFolder);
        }
        else
        {
          Logger.Trace("Copying app files from {0} to {1}. ", _deleteSettings.RootFolder, appTargetFolder);
          Helpers.DeepCopy(new DirectoryInfo(_deleteSettings.RootFolder), new DirectoryInfo(appTargetFolder), "node_modules|.git", true);
          Logger.Trace("Done copying app files from {0} to {1}. ", _deleteSettings.RootFolder, appTargetFolder);
        }
      }
      Logger.Trace("Zipping folder {0} into zipfile {1}. ", _tempFolder, fileName);
      ZipFile.CreateFromDirectory(_tempFolder, fileName);
      Logger.Trace("Done zipping folder {0} into zipfile {1}. ", _tempFolder, fileName);
      Logger.Trace("Deleting folder {0}", _tempFolder);
      Helpers.MakeWritable(_tempFolder);
      Directory.Delete(_tempFolder, true);
      Logger.Trace("Done deleting folder {0}", _tempFolder);
    }

    private void DeleteIisSite()
    {
      var serverManager = new ServerManager();
      serverManager.Sites.First(x => x.Name == _deleteSettings.SiteName).Delete();
      serverManager.CommitChanges();
    }

    private DeleteSettings GetDeleteSettings()
    {
      var deleteSettings = new DeleteSettings
      {
        FilesFolder = _site.GetFilesPath(),
        RootFolder = _site.GetRootPath(),
        SiteName = _site.Name,
        BackupFolder = Configuration.GetAppSetting("BackupFolder"),
        IsCustomSolution = _site.IsCustomSolution(),
        DatabaseName = _site.GetDatabaseName(),
        DatabaseServer = _site.GetSqlServerInstance()
      };
      return deleteSettings;
    }
  }
}
